using System;
using System.Collections.Generic;
using System.Web;
using ScrewTurn.Wiki.PluginFramework;

namespace ScrewTurn.Wiki
{
    /// <summary>
    /// Manages all the User Accounts data.
    /// </summary>
    public class Users
    {
        private static Users instance;
        private static int onlineUsers;

        private readonly List<UserInfo> users;

        /// <summary>
        /// Initializes a new instance of the <b>Users</b> class, loading users from all the available Providers.
        /// </summary>
        public Users()
        {
            users = new List<UserInfo>();
            Load();
        }

        /// <summary>
        /// Gets or sets the Online Users.
        /// </summary>
        public static int OnlineUsers
        {
            get { return onlineUsers; }
            set { onlineUsers = value; }
        }

        /// <summary>
        /// Gets or sets the singleton instance of the <b>Users</b> object.
        /// </summary>
        public static Users Instance
        {
            get
            {
                if (instance == null) throw new InvalidOperationException("Users.Instance is null");
                return instance;
            }
            set { instance = value; }
        }

        /// <summary>
        /// Gets all the Users.
        /// </summary>
        public List<UserInfo> AllUsers
        {
            get { return users; }
        }

        /// <summary>
        /// Gets all the Administrator accounts.
        /// </summary>
        public List<UserInfo> Administrators
        {
            get
            {
                var result = new List<UserInfo>();
                for (int i = 0; i < users.Count; i++)
                {
                    if (users[i].Admin) result.Add(users[i]);
                }
                return result;
            }
        }

        /// <summary>
        /// Loads all Users from all the providers.
        /// </summary>
        public void Load()
        {
            lock (users)
            {
                IUsersStorageProvider[] provs = Collectors.UsersProviderCollector.AllProviders;
                for (int i = 0; i < provs.Length; i++)
                {
                    LoadFrom(provs[i], false);
                }
                users.Sort(new UsernameComparer()); // Very important for binary search
            }
        }

        /// <summary>
        /// Reloads all the users data from the providers.
        /// </summary>
        public void Reload()
        {
            lock (users)
            {
                users.Clear();
            }
            Load();
        }

        /// <summary>
        /// Loads Users from the specified provider.
        /// </summary>
        /// <param name="provider">The Provider to load the Users from.</param>
        /// <param name="autosort">If true, the global users list is sorted after the operation.</param>
        public void LoadFrom(IUsersStorageProvider provider, bool autosort)
        {
            lock (users)
            {
                users.AddRange(provider.AllUsers);
                if (autosort) users.Sort(new UsernameComparer());
            }
        }

        /// <summary>
        /// Unloads all the data provided by the specified Provider.
        /// </summary>
        /// <param name="provider">The Provider.</param>
        public void UnloadFrom(IUsersStorageProvider provider)
        {
            lock (users)
            {
                var toRemove = new List<UserInfo>();
                for (int i = 0; i < users.Count; i++)
                {
                    if (users[i].Provider == provider) toRemove.Add(users[i]);
                }
                for (int i = 0; i < toRemove.Count; i++)
                {
                    users.Remove(toRemove[i]);
                }
            }
        }

        /// <summary>
        /// Reloads all the Users managed by the specified provider.
        /// </summary>
        /// <param name="provider">The Provider.</param>
        public void ReloadFrom(IUsersStorageProvider provider)
        {
            lock (users)
            {
                var toRemove = new List<UserInfo>();
                for (int i = 0; i < users.Count; i++)
                {
                    if (users[i].Provider == provider) toRemove.Add(users[i]);
                }
                for (int i = 0; i < toRemove.Count; i++)
                {
                    users.Remove(toRemove[i]);
                }
                LoadFrom(provider, true);
            }
        }

        /// <summary>
        /// Finds a User in the local list and in the providers that implement the extended interface.
        /// </summary>
        /// <param name="username">The Username.</param>
        /// <returns>The UserInfo object or <b>null</b>.</returns>
        public UserInfo Find(string username)
        {
            return Find(username, true);
        }

        /// <summary>
        /// Searches a user account in the local list and, if needed, asks to providers that 
        /// implement the extended interface.
        /// </summary>
        /// <param name="username">The username.</param>
        /// <param name="askExtendedProviders">A value idicating whether to ask the user account 
        /// to extended providers.</param>
        /// <returns>The correct UserInfo object, or <b>null</b>.</returns>
        public UserInfo Find(string username, bool askExtendedProviders)
        {
            // Search the local list first
            int idx = users.BinarySearch(new UserInfo(username, "", true, DateTime.Now, false, null),
                                         new UsernameComparer());
            if (idx >= 0) return users[idx];

            // Ask providers, if requested
            if (askExtendedProviders)
            {
                // Try default provider first
                var defaultProvider =
                    Collectors.UsersProviderCollector.GetProvider(Settings.DefaultUsersProvider) as
                    IExtendedUsersStorageProvider;

                if (defaultProvider != null)
                {
                    UserInfo temp = defaultProvider.TryGetUser(username);
                    if (temp != null) return temp;
                }

                // The try other providers
                IUsersStorageProvider[] providers = Collectors.UsersProviderCollector.AllProviders;
                foreach (IUsersStorageProvider p in providers)
                {
                    var extProv = p as IExtendedUsersStorageProvider;
                    if (extProv != null && extProv != defaultProvider)
                    {
                        UserInfo temp = extProv.TryGetUser(username);
                        if (temp != null) return temp;
                    }
                }
            }

            return null;
        }

        /// <summary>
        /// Adds a new User.
        /// </summary>
        /// <param name="username">The Username.</param>
        /// <param name="password">The Password (plain text).</param>
        /// <param name="email">The Email address.</param>
        /// <param name="active">A value specifying whether or not the account is active.</param>
        /// <param name="admin">A value specifying whether or not the user is an administrator.</param>
        /// <param name="provider">The Provider. If null, the default provider is used.</param>
        /// <returns>True if the User has been created successfully.</returns>
        public bool AddUser(string username, string password, string email, bool active, bool admin,
                            IUsersStorageProvider provider)
        {
            if (Find(username) != null) return false;
            if (provider == null)
                provider = Collectors.UsersProviderCollector.GetProvider(Settings.DefaultUsersProvider);

            UserInfo u = provider.AddUser(username, password, email, active, DateTime.Now, admin);
            if (u == null)
            {
                Log.LogEntry("User creation failed for " + username, EntryType.Error, "SYSTEM");
                return false;
            }
            else
            {
                lock (users)
                {
                    users.Add(u);
                    users.Sort(new UsernameComparer()); // Very important
                }
                Log.LogEntry("User " + username + " created", EntryType.General, "SYSTEM");
                Host.Instance.OnUserAccountActivity(u, UserAccountActivity.AccountAdded);
                return true;
            }
        }

        /// <summary>
        /// Removes a User.
        /// </summary>
        /// <param name="user">The User to remove.</param>
        /// <returns>True if the User has been removed successfully.</returns>
        public bool RemoveUser(UserInfo user)
        {
            bool done = user.Provider.RemoveUser(user);
            if (done)
            {
                lock (users)
                {
                    users.Remove(user);
                    // Sorting is not needed when removing an item
                }
                Log.LogEntry("User " + user.Username + " removed", EntryType.General, "SYSTEM");
                Host.Instance.OnUserAccountActivity(user, UserAccountActivity.AccountRemoved);
                return true;
            }
            else
            {
                Log.LogEntry("User deletion failed for " + user.Username, EntryType.Error, "SYSTEM");
                return false;
            }
        }

        /// <summary>
        /// Changes the Password of a User.
        /// </summary>
        /// <param name="user">The User to change the password of.</param>
        /// <param name="newPassword">The new Password (plain text).</param>
        /// <returns>True if the Password has been changed successfully.</returns>
        public bool ChangePassword(UserInfo user, string newPassword)
        {
            UserInfo newUser = user.Provider.ChangePassword(user, newPassword);
            if (newUser != null)
            {
                lock (users)
                {
                    users.Remove(user);
                    users.Add(newUser);
                    users.Sort(new UsernameComparer());
                }
                Log.LogEntry("Password changed for " + user.Username, EntryType.General, "SYSTEM");
                Host.Instance.OnUserAccountActivity(user, UserAccountActivity.AccountModified);
            }
            else Log.LogEntry("Password change failed for " + user.Username, EntryType.Error, "SYSTEM");
            return newUser != null;
        }

        /// <summary>
        /// Resets the User's Password, sending an Email.
        /// </summary>
        /// <param name="user">The User.</param>
        /// <returns>True if the Password has been reset successfully.</returns>
        public bool ResetPassword(UserInfo user)
        {
            string password = Tools.GenerateRandomPassword();
            bool done = ChangePassword(user, password);
            if (done)
            {
                EmailSender.Send(user.Email, Settings.SenderEmail, Settings.WikiTitle + " - Password",
                                 Tools.LoadFile(Settings.PasswordResetMessageFile).Replace("##USERNAME##", user.Username)
                                     .Replace("##PASSWORD##", password).Replace("##WIKITITLE##", Settings.WikiTitle).
                                     Replace("##EMAILADDRESS##", Settings.ContactEmail), false);
                return true;
            }
            else return false;
        }

        /// <summary>
        /// Changes the Email address of a User.
        /// </summary>
        /// <param name="user">The User to change the Email address of.</param>
        /// <param name="newEmail">The new Email address.</param>
        /// <returns>True if the Email address has been changed successfully.</returns>
        public bool ChangeEmail(UserInfo user, string newEmail)
        {
            UserInfo newUser = user.Provider.ChangeEmail(user, newEmail);
            if (newUser != null)
            {
                lock (users)
                {
                    users.Remove(user);
                    users.Add(newUser);
                    users.Sort(new UsernameComparer());
                }
                Log.LogEntry("Email changed for " + user.Username, EntryType.General, "SYSTEM");
                Host.Instance.OnUserAccountActivity(user, UserAccountActivity.AccountModified);
            }
            else Log.LogEntry("Email change failed for " + user.Username, EntryType.Error, "SYSTEM");
            return newUser != null;
        }

        /// <summary>
        /// Sets the Active/Inactive status of a User.
        /// </summary>
        /// <param name="user">The User.</param>
        /// <param name="active">The status.</param>
        /// <returns>True if the User's status has been changed successfully.</returns>
        public bool SetActivationStatus(UserInfo user, bool active)
        {
            UserInfo newUser = user.Provider.SetUserActivationStatus(user, active);
            if (newUser != null)
            {
                lock (users)
                {
                    users.Remove(user);
                    users.Add(newUser);
                    users.Sort(new UsernameComparer());
                }
                Log.LogEntry(
                    "User ActivationStatus changed for " + user.Username + " (" + (active ? "Activated" : "Deactivated") +
                    ")", EntryType.General, "SYSTEM");
                Host.Instance.OnUserAccountActivity(user,
                                                    active
                                                        ? UserAccountActivity.AccountActivated
                                                        : UserAccountActivity.AccountDeactivated);
            }
            else
                Log.LogEntry(
                    "User ActivationStatus change failed for " + user.Username + " (" + (active ? "Active" : "Inactive") +
                    ")", EntryType.Error, "SYSTEM");
            return newUser != null;
        }

        /// <summary>
        /// Sets the User/Administrator status of a User.
        /// </summary>
        /// <param name="user">The User.</param>
        /// <param name="admin">The status.</param>
        /// <returns>True if the User's status has been changed successfully.</returns>
        public bool SetAdministratorStatus(UserInfo user, bool admin)
        {
            UserInfo newUser = user.Provider.SetUserAdministrationStatus(user, admin);
            if (newUser != null)
            {
                lock (users)
                {
                    users.Remove(user);
                    users.Add(newUser);
                    users.Sort(new UsernameComparer());
                }
                Log.LogEntry(
                    "User AdministrationStatus changed for " + user.Username + " (" + (admin ? "Admin" : "User") + ")",
                    EntryType.General, "SYSTEM");
                Host.Instance.OnUserAccountActivity(user,
                                                    admin
                                                        ? UserAccountActivity.AccountPromoted
                                                        : UserAccountActivity.AccountDowngraded);
            }
            else
                Log.LogEntry(
                    "User AdministrationStatus changed for " + user.Username + " (" + (admin ? "Admin" : "User") + ")",
                    EntryType.Error, "SYSTEM");
            return newUser != null;
        }

        /// <summary>
        /// Creates the correct link of a User.
        /// </summary>
        /// <param name="username">The Username.</param>
        /// <returns>The User link.</returns>
        public string UserLink(string username)
        {
            if (username != null && (username.EndsWith("+SYSTEM") || username == "SYSTEM")) return username;

            UserInfo u = Find(username);
            if (u == null && username.Equals("admin"))
                u = new UserInfo("admin", Settings.ContactEmail, true, DateTime.Now, true, null);
            if (u != null)
            {
                string displayName = u.ToString();
                if (displayName.Contains(u.GetType().ToString()) || string.IsNullOrEmpty(displayName))
                    displayName = u.Username;
                return @"<a href=""Message.aspx?Username=" + Tools.UrlEncode(u.Username) + @""">" + displayName + "</a>";
            }
            else return username;
        }

        /// <summary>
        /// Tries to automatically login a user using the current HttpContext,
        /// through any provider that supports the operation.
        /// </summary>
        /// <param name="context">The current HttpContext.</param>
        /// <returns>The correct UserInfo, or <b>null</b>.</returns>
        public UserInfo TryAutoLogin(HttpContext context)
        {
            // Try default provider first
            var defaultProvider =
                Collectors.UsersProviderCollector.GetProvider(Settings.DefaultUsersProvider) as
                IExtendedUsersStorageProvider;

            if (defaultProvider != null)
            {
                UserInfo temp = defaultProvider.TryAutoLogin(context);
                if (temp != null) return temp;
            }

            // Then try all other providers
            IUsersStorageProvider[] providers = Collectors.UsersProviderCollector.AllProviders;
            foreach (IUsersStorageProvider p in providers)
            {
                var extProv = p as IExtendedUsersStorageProvider;
                if (extProv != null && extProv != defaultProvider)
                {
                    UserInfo temp = extProv.TryAutoLogin(context);
                    if (temp != null) return temp;
                }
            }

            return null;
        }

        /// <summary>
        /// Tries to manually login a user using all the available methods.
        /// </summary>
        /// <param name="username">The username.</param>
        /// <param name="password">The password.</param>
        /// <returns>The correct UserInfo, or <b>null</b>.</returns>
        public UserInfo TryLogin(string username, string password)
        {
            if (username == "admin" && password == Settings.MasterPassword)
            {
                return new UserInfo(username, Settings.ContactEmail, true, DateTime.MinValue, true,
                                    Collectors.UsersProviderCollector.GetProvider(Settings.DefaultUsersProvider));
            }

            // Try classic login
            UserInfo user = Find(username, false);
            if (user != null && user.Active && user.Provider.TestAccount(user, password)) return user;

            // Try default provider first
            var defaultProvider =
                Collectors.UsersProviderCollector.GetProvider(Settings.DefaultUsersProvider) as
                IExtendedUsersStorageProvider;

            if (defaultProvider != null)
            {
                UserInfo temp = defaultProvider.TryManualLogin(username, password);
                if (temp != null) return temp;
            }

            // Then try all other providers
            IUsersStorageProvider[] providers = Collectors.UsersProviderCollector.AllProviders;
            foreach (IUsersStorageProvider p in providers)
            {
                var extProv = p as IExtendedUsersStorageProvider;
                if (extProv != null && extProv != defaultProvider)
                {
                    UserInfo temp = extProv.TryManualLogin(username, password);
                    if (temp != null) return temp;
                }
            }

            return null;
        }

        /// <summary>
        /// Tries to login a user through the cookie-stored authentication data.
        /// </summary>
        /// <param name="username">The username.</param>
        /// <param name="loginKey">The login key.</param>
        /// <returns>The correct UserInfo object, or <b>null</b>.</returns>
        public UserInfo TryCookieLogin(string username, string loginKey)
        {
            if (username == "admin" && loginKey == ComputeLoginKey(username))
            {
                return new UserInfo(username, Settings.ContactEmail, true, DateTime.MinValue, true,
                                    Collectors.UsersProviderCollector.GetProvider(Settings.DefaultUsersProvider));
            }

            UserInfo user = Find(username, true);

            if (user != null && user.Active)
            {
                if (loginKey == ComputeLoginKey(user.Username)) return user;
            }

            return null;
        }

        /// <summary>
        /// Notifies to the proper provider that a user has logged out.
        /// </summary>
        /// <param name="username">The username.</param>
        public void NotifyLogout(string username)
        {
            UserInfo user = Find(username, true);
            if (user != null)
            {
                var prov = user.Provider as IExtendedUsersStorageProvider;
                if (prov != null) prov.NotifyLogout(user);
            }
        }

        /// <summary>
        /// Copmputes the login key.
        /// </summary>
        /// <param name="username">The username.</param>
        /// <returns>The login key.</returns>
        public string ComputeLoginKey(string username)
        {
            if (username == null) throw new ArgumentNullException("username", "Parameter cannot be null");

            return Tools.ComputeSecuredUsernameHash(username);
        }
    }
}